Skip to content

Set Windows DACL on discovery directory under LOCALAPPDATA - #5951

Open
stantheman0128 wants to merge 3 commits into
stacklok:mainfrom
stantheman0128:fix/5217-windows-dacl-discovery
Open

Set Windows DACL on discovery directory under LOCALAPPDATA#5951
stantheman0128 wants to merge 3 commits into
stacklok:mainfrom
stantheman0128:fix/5217-windows-dacl-discovery

Conversation

@stantheman0128

@stantheman0128 stantheman0128 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

writeServerInfoTo created the discovery directory under xdg.StateHome (%LOCALAPPDATA% on Windows) with os.MkdirAll + os.Chmod(0700). POSIX modes are advisory on NTFS, so inherited ACEs from the parent (Everyone, other interactive users) survive, and any local account holding Modify can rewrite server.json. With the npipe:// discovery URL from #5201 / #5214, that file is what decides where the next client connects.

What changed:

  • Windows sets an explicit protected DACL granting FILE-equivalent GenericAll to the process user and SYSTEM only, with OICI so server.json inherits the same restriction. Non-Windows keeps os.Chmod.
  • The lockdown now runs in a new discovery.EnsureSecureDir, called from writeDiscoveryFile before server.json.lock is taken and before Discover runs, instead of only inside the write.
  • The chain ToolHive creates is covered, not just the leaf: %LOCALAPPDATA%\toolhive and %LOCALAPPDATA%\toolhive\server.
  • Directory ownership is validated before the lockdown counts as complete, and fails closed otherwise.
  • readServerInfoFrom rejects a server.json owned by another account, alongside the existing symlink rejection.

Fixes #5217

Threat model (corrected)

The first revision of this description claimed a spoofed endpoint could capture the discovery nonce. That is wrong, and @samuv is right to flag it: CheckHealth sends a plain GET /health with no nonce in the request and compares the nonce that comes back in the response header, so nothing is transmitted to the endpoint being probed. An attacker who can rewrite server.json picks the nonce themselves.

The actual risks of an attacker-writable discovery file are:

  • Endpoint spoofing. Clients (CLI, Studio) read the URL from server.json and dial the attacker's pipe or address.
  • Persistent denial of startup. A planted file whose endpoint answers /health with the matching nonce makes Discover report StateRunning, so every thv serve aborts with "another ToolHive server is already running".
  • Interception or forgery of later client API traffic, once a client has been pointed at that endpoint.

Review round 2: the three findings

1. The restriction ran after startup had already trusted the directory (discovery.go:83).

writeDiscoveryFile did MkdirAll then WithFileLock then Discover then WriteServerInfo, so the DACL landed last. Two consequences: server.json.lock was created in a directory other accounts could still write to, and a StateRunning result returns before WriteServerInfo is ever reached, so the directory stayed loose for the whole startup while its planted contents were believed.

discovery.EnsureSecureDir() now runs first, and WriteServerInfo also calls it so the public write path cannot regress. TestWriteDiscoveryFile_RestrictsDirBeforeTrustingExistingFile drives the real writeDiscoveryFile with a planted, healthy-looking file and asserts the chain is already protected on the failing return. Without the reordering that test fails with D:AI(A;OICIID;0x1301bf;;;WD)... on both directories (output below).

2. The intermediate toolhive directory is protected too (permissions_windows.go:23).

Correct: restricting only the leaf left the intermediate with inherited Modify, and Modify carries DELETE on that object, so another interactive user could rename toolhive aside and recreate toolhive\server with an ACL of their own. EnsureSecureDir walks the chain outermost first and restricts each directory ToolHive creates.

Above that chain there is a documented invariant rather than more ACL rewriting, because %LOCALAPPDATA% belongs to the OS and to other applications: ToolHive relies on its default protected ACL (user, SYSTEM, Administrators). If another account does hold delete-child there, it can still swap the chain aside, and the ownership check in finding 3 is what turns that into a startup failure instead of silent trust. That reasoning is in a comment at the top of permissions_windows.go.

3. Ownership is validated, fail closed (permissions_windows.go:58).

Also correct, and it was the sharpest of the three: owners keep WRITE_DAC implicitly, so a DACL written onto a directory somebody else pre-created is cosmetic. restrictDiscoveryDir now checks the owner before writing the DACL (so a hostile directory is never half-secured) and again afterwards (so a pre-creating owner that raced the write cannot be reported as a completed lockdown).

Trusted owners are the process user, SYSTEM, and Administrators. Administrators is included deliberately: a directory created by an elevated thv serve is owned by Administrators rather than by the user, and a local administrator is outside this threat model since they can take ownership of anything. It does not weaken the check against the attacker in scope, because a standard user cannot make SYSTEM or Administrators the owner of a directory they create. Taking ownership instead of failing was considered and rejected: it would silently repair the container while leaving the attacker's server.json inside it to be trusted.

One thing the three findings surfaced together: an already-planted server.json is trusted even with the directory fixed, so the read path needed the same treatment. Changing the parent DACL does re-propagate inherited ACEs to existing children (visible in the Evidence below), but explicit ACEs and ownership are untouched, so readServerInfoFrom now refuses a discovery file owned by another account. On startup the effect is self-healing rather than fatal: Discover surfaces the error, startup proceeds on its existing "discovery check failed" path, and AtomicWriteFile replaces the planted file with one that inherits the protected DACL.

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (go test ./pkg/server/discovery/ ./pkg/api/ on Windows 11, go1.26.5; task is not installed on this machine)
  • E2E tests (task test-e2e)
  • Linting (golangci-lint run at v2.6.2 with the repo .golangci.yml, for GOOS=linux and GOOS=windows; go vet for linux, darwin, windows)
  • Manual testing (real %LOCALAPPDATA% product path, below)

Evidence

Production ordering, before the fix. Reverting only the pkg/api reordering and running the new test shows exactly the gap that was reported: startup aborts on the planted file and both directories are still inheriting (D:AI) with Everyone (WD) on them.

--- FAIL: TestWriteDiscoveryFile_RestrictsDirBeforeTrustingExistingFile
    Messages: DACL of ...\001\toolhive must be protected against inheritance:
      D:AI(A;OICIID;0x1301bf;;;WD)(A;OICIID;0x1301bf;;;S-1-5-21-349393094-...)
      (A;OICIID;FA;;;S-1-5-21-2310101069-...-1001)(A;OICIID;FA;;;SY)(A;OICIID;FA;;;BA)
    Messages: DACL of ...\001\toolhive\server must be protected against inheritance:
      D:AI(A;OICIID;0x1301bf;;;WD)...

Unit tests, after the fix (Windows 11, go1.26.5):

=== RUN   TestEnsureSecureDirIn_CreatesAndRestrictsChain
=== RUN   TestEnsureSecureDirIn_TightensExistingChain
=== RUN   TestWriteServerInfo_WindowsDACL_NoOtherInteractiveUsers
=== RUN   TestRestrictDiscoveryDirPermissions_ReplacesExistingLooseACL
=== RUN   TestRestrictDiscoveryDirPermissions_NewDirectory
=== RUN   TestEnsureSecureDirIn_ProtectsIntermediateToolhiveDir
=== RUN   TestRestrictDiscoveryDir_FailsClosedOnUntrustedOwner
=== RUN   TestRestrictDiscoveryDir_AcceptsCurrentUserOwner
=== RUN   TestValidateFileOwner_RejectsUntrustedOwner
--- SKIP: TestEnsureSecureDirIn_TightensExistingChain (0.00s)
--- PASS: TestRestrictDiscoveryDir_AcceptsCurrentUserOwner (0.01s)
--- PASS: TestEnsureSecureDirIn_CreatesAndRestrictsChain (0.01s)
--- PASS: TestRestrictDiscoveryDir_FailsClosedOnUntrustedOwner (0.02s)
--- PASS: TestValidateFileOwner_RejectsUntrustedOwner (0.02s)
--- PASS: TestRestrictDiscoveryDirPermissions_ReplacesExistingLooseACL (0.02s)
--- PASS: TestRestrictDiscoveryDirPermissions_NewDirectory (0.02s)
--- PASS: TestEnsureSecureDirIn_ProtectsIntermediateToolhiveDir (0.02s)
--- PASS: TestWriteServerInfo_WindowsDACL_NoOtherInteractiveUsers (0.02s)
ok  	github.com/stacklok/toolhive/pkg/server/discovery	1.337s

=== RUN   TestWriteDiscoveryFile_RestrictsDirBeforeTrustingExistingFile
--- PASS: TestWriteDiscoveryFile_RestrictsDirBeforeTrustingExistingFile (0.03s)
ok  	github.com/stacklok/toolhive/pkg/api	1.441s

Product path: real %LOCALAPPDATA%, real discovery.EnsureSecureDir. The chain was pre-created loose (Everyone inheritable Modify on the base, plain MkdirAll for the children) with a planted server.json, the way an earlier run plus a local attacker would leave it.

Before:

...\toolhive-5217-evidence\toolhive         Everyone:(I)(OI)(CI)(M)
                                            NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
                                            BUILTIN\Administrators:(I)(OI)(CI)(F)
                                            CHIAO-BASE\stans:(I)(OI)(CI)(F)
SDDL: ...D:AI(A;OICIID;0x1301bf;;;WD)(A;OICIID;FA;;;SY)(A;OICIID;FA;;;BA)(A;OICIID;FA;;;<user>)

...\toolhive-5217-evidence\toolhive\server  Everyone:(I)(OI)(CI)(M)
                                            NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
                                            BUILTIN\Administrators:(I)(OI)(CI)(F)
                                            CHIAO-BASE\stans:(I)(OI)(CI)(F)
SDDL: ...D:AI(A;OICIID;0x1301bf;;;WD)...

After discovery.EnsureSecureDir():

...\toolhive-5217-evidence\toolhive         CHIAO-BASE\stans:(F)
                                            CHIAO-BASE\stans:(OI)(CI)(IO)(F)
                                            NT AUTHORITY\SYSTEM:(F)
                                            NT AUTHORITY\SYSTEM:(OI)(CI)(IO)(F)
SDDL: ...D:PAI(A;OICIIO;GA;;;SY)(A;;FA;;;SY)(A;OICIIO;GA;;;<user>)(A;;FA;;;<user>)

...\toolhive-5217-evidence\toolhive\server  CHIAO-BASE\stans:(F)
                                            CHIAO-BASE\stans:(OI)(CI)(IO)(F)
                                            NT AUTHORITY\SYSTEM:(F)
                                            NT AUTHORITY\SYSTEM:(OI)(CI)(IO)(F)
SDDL: ...D:PAI(A;OICIIO;GA;;;SY)(A;;FA;;;SY)(A;OICIIO;GA;;;<user>)(A;;FA;;;<user>)

...\toolhive\server\server.json             CHIAO-BASE\stans:(I)(F)
                                            NT AUTHORITY\SYSTEM:(I)(F)

Both directories end up D:P (protected) with the user and SYSTEM only, on both the intermediate and the leaf. The pre-existing server.json shows the inherited-ACE re-propagation mentioned above: Everyone is gone from it without the file being rewritten. That is also why the read path checks ownership rather than trusting the ACL alone.

What was not tested

  • The thv serve binary end to end. There is no container runtime on this machine (docker info fails, and pkg/api tests that build the full server already fail on no available runtime found at origin/main), so startup aborts before it reaches the discovery step. The new pkg/api test calls the real writeDiscoveryFile on the real path instead, and the product-path Evidence above uses the real exported discovery.EnsureSecureDir against %LOCALAPPDATA%.
  • A genuine cross-account hostile owner. Creating a directory owned by another account needs a second user or SeRestorePrivilege, so the hostile-owner tests inject the trusted-owner set against a directory the test user really owns, which exercises the same failure path.
  • task test / task lint-fix (Task is not installed here). golangci-lint run was run directly with the repo config for both GOOS=linux and GOOS=windows: the only findings are three pre-existing gci reports on files this PR does not touch (pkg/api/docs.go, openapi.go, scalar.go), caused by CRLF in this local Windows checkout.
  • Five test failures in pkg/server/discovery on this machine are pre-existing and identical at origin/main: the two symlink tests (os.Symlink needs SeCreateSymbolicLinkPrivilege) and three unix-socket path tests. This branch turns three origin/main failures into skips (the POSIX-mode assertions).

API Compatibility

  • This PR does not break the v1beta1 API, OR the api-break-allowed label is applied and the migration guidance is described above.

Changes

File Change
pkg/api/server.go writeDiscoveryFile calls discovery.EnsureSecureDir() before the lock and Discover
pkg/api/discovery_permissions_windows_test.go Production-order regression test
pkg/server/discovery/discovery.go EnsureSecureDir, directory chain helper, owner check on the read path
pkg/server/discovery/permissions_windows.go Owner validation (fail closed) around the protected DACL replace, ancestor invariant documented
pkg/server/discovery/permissions_other.go Non-Windows os.Chmod plus a no-op owner check
pkg/server/discovery/permissions_windows_test.go Ancestor replacement, hostile owner, read-path rejection
pkg/server/discovery/discovery_test.go Chain creation and tightening, POSIX modes

Does this introduce a user-facing change?

On Windows, %LOCALAPPDATA%\...\toolhive and its server subdirectory are locked down to the user that started thv plus SYSTEM, and this now happens at the start of thv serve rather than at the end. Two new fail-closed startup errors are possible where startup previously proceeded silently: a discovery directory owned by another account, and a server.json owned by another account. Both name the path and say what to do. No CLI flag or config change.

Special notes for reviewers

  • The ownership trust set (process user, SYSTEM, Administrators) is the one judgement call I would most like a second opinion on. The alternative, accepting only the process user, fails on any machine where thv serve has ever been run elevated.
  • Matches the named-pipe DACL intent from Restrict Windows named-pipe DACL to creating user #5214 (user + SYSTEM only), applied to the directory that stores the npipe:// URL.
  • The corrected threat-model wording is in the new commit message; the misleading nonce sentence lives only in the first commit's message, which the squash merge will replace with this description. Happy to squash the branch if you would rather it not appear in the branch history at all.
  • AI assistance: Cursor helped implement and verify this round; I reviewed the diff, ran the Windows tests, captured the %LOCALAPPDATA% before and after ACLs, and confirmed the pre-fix failure by reverting the reordering.

Made with Cursor

os.Chmod is advisory on NTFS, so the discovery directory under
%LOCALAPPDATA% could keep inherited ACEs (Everyone, other interactive
users). A local attacker who can write server.json can redirect the
npipe URL and steal the discovery nonce on the next health check.

On Windows, replace the directory DACL with a protected ACL granting
GenericAll only to the process user and SYSTEM (create and existing-dir
paths). Non-Windows keeps os.Chmod. Windows tests assert no other
interactive users remain; POSIX mode assertions skip on Windows.

Fixes stacklok#5217

Signed-off-by: stantheman0128 <stanshih888@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@stantheman0128

Copy link
Copy Markdown
Contributor Author

Assignment ack is on #5217. PR is ready for review when you have bandwidth.

DCO Signed-off-by is on the commit; Windows DACL tests included. Happy to adjust scope if you want a different approach.

Stan Shih (@stantheman0128)

@samuv samuv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this. The explicit protected DACL and adversarial Windows tests are a solid direction. I found three security gaps that seem important to resolve before merge: the restriction runs only after startup has already trusted the discovery file, the protected leaf remains replaceable through the intermediate directory, and hostile ownership can survive the DACL replacement. I left focused inline comments with suggested fixes and regression coverage.

One threat-model wording note: the current health request does not transmit the discovery nonce. The concrete risks are endpoint spoofing, persistent startup denial, and interception or forgery of later client API traffic, so the PR and commit description should be adjusted accordingly.

Checklist

  • Tests: Good Windows create and replace coverage; additional production-order, ancestor-replacement, and hostile-owner tests are needed. The PR currently has no checks, and the repository test workflow is Ubuntu-only.
  • Docs: No user-facing documentation appears necessary, but the nonce-leak wording should be corrected.
  • Registry impact: None.
  • Security: Requesting changes for the three inline findings.
  • Backwards compatibility: The non-Windows behavior and error text appear preserved; unsupported Windows filesystems remain an explicit fail-closed compatibility tradeoff.

return fmt.Errorf("failed to set discovery directory permissions: %w", err)
// On Windows this sets an explicit protected DACL (POSIX modes are
// advisory on NTFS); elsewhere it is os.Chmod(dirPermissions).
if err := restrictDiscoveryDirPermissions(dir); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move this restriction earlier, before the startup path acquires server.json.lock and calls discovery.Discover? In pkg/api/server.go, writeDiscoveryFile currently does MkdirAll → WithFileLock → Discover → WriteServerInfo, so an existing loose directory is trusted before this function runs. An attacker can write a server.json with an attacker-controlled pipe and nonce, return that nonce from /health, and make Discover report StateRunning; the caller then returns before WriteServerInfo, so the DACL is never repaired. I think the permission setup needs to happen before the lock and Discover, with a regression test covering that production ordering.

//
// Inheritance (OICI) is intentional: server.json and any future children
// pick up the same restriction instead of inheriting a looser parent ACL.
func restrictDiscoveryDirPermissions(dir string) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also protect or validate the intermediate toolhive directory? The adversarial test grants Everyone inheritable Modify on the parent and then creates parent\toolhive\server, but this function restricts only the server leaf. That leaves toolhive with inherited Modify; Modify includes DELETE on that object and directory-creation rights in its parent, so another user can rename toolhive aside and recreate toolhive\server, bypassing the protected leaf. Please secure the relevant path chain, or enforce and document a restrictive-ancestor invariant, and extend the Windows test to cover ancestor replacement.

return fmt.Errorf("failed to build discovery directory DACL: %w", err)
}

if err := windows.SetNamedSecurityInfo(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we validate the directory owner before considering the lockdown complete? This call replaces only the DACL and passes a nil owner. If another user pre-created the directory and granted the process enough rights for this operation to succeed, that user remains the owner; Windows owners retain the ability to change the DACL and can make it permissive again. Please either fail closed when the owner is not the process user or securely establish the expected owner, and add coverage for a pre-existing directory with hostile ownership.

@github-actions github-actions Bot added the size/S Small PR: 100-299 lines changed label Jul 29, 2026
Review follow-up. The DACL work only ran inside writeServerInfoTo, which is
the last step of startup: pkg/api creates the directory, takes
server.json.lock inside it, and runs Discover before it ever writes. A
StateRunning result returns before the write, so a directory left loose by
an earlier run stayed loose for the whole startup, and the server.json in it
was trusted regardless.

Move the lockdown into discovery.EnsureSecureDir and call it from
writeDiscoveryFile ahead of the lock and Discover. Restrict the intermediate
toolhive directory as well as the server leaf: inherited Modify on the
intermediate carries DELETE, so another interactive user could rename it
aside, recreate the chain with an ACL of their own, and leave the protected
leaf unread.

Validate ownership before treating a directory as locked down. Owners keep
WRITE_DAC implicitly, so replacing the DACL of a directory somebody else
pre-created is cosmetic, since that owner can make it permissive again. Fail
closed unless the owner is the process user, SYSTEM, or Administrators; a
standard user cannot create a directory owned by any of those, and an
elevated thv serve legitimately leaves Administrators as owner. Apply the
same check to server.json on the read path: changing the parent DACL
re-propagates inherited ACEs but leaves explicit ACEs and ownership alone,
and that file's URL and nonce decide where clients send API traffic.

Correct the threat model this branch described. The health request does not
transmit the nonce, so a spoofed endpoint learns nothing. The risks are
endpoint spoofing, persistent denial of startup, and interception or forgery
of later client API traffic.

Non-Windows behavior is unchanged: os.Chmod over the same chain, and the
owner check is a documented no-op because POSIX modes are enforced rather
than advisory.

Tests: production ordering (Discover reports StateRunning, startup aborts,
both directories are already protected), ancestor replacement (intermediate
and leaf both protected), and hostile ownership (fail closed with the loose
ACE left intact, plus the read-path rejection).

Fixes stacklok#5217

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: stantheman0128 <stanshih888@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/S Small PR: 100-299 lines changed size/L Large PR: 600-999 lines changed labels Jul 29, 2026
The helper listed the SDDL aliases of the groups a local attacker could come
from and asserted their absence. Enumerate the ACEs and require every
trustee to be SYSTEM or the process user instead: that also covers trustees
the list did not name, and it drops a bare alias literal that codespell
reads as a misspelling.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: stantheman0128 <stanshih888@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jul 29, 2026
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.46154% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.53%. Comparing base (c007e6a) to head (e9ef6a2).
⚠️ Report is 100 commits behind head on main.

Files with missing lines Patch % Lines
pkg/server/discovery/discovery.go 30.00% 10 Missing and 4 partials ⚠️
pkg/server/discovery/permissions_other.go 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5951      +/-   ##
==========================================
+ Coverage   71.80%   72.53%   +0.73%     
==========================================
  Files         708      735      +27     
  Lines       72767    75939    +3172     
==========================================
+ Hits        52250    55084    +2834     
- Misses      16778    16936     +158     
- Partials     3739     3919     +180     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@stantheman0128

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All three findings held, and the wording note was right.

Ordering. discovery.EnsureSecureDir() now runs in writeDiscoveryFile before server.json.lock and before Discover, and WriteServerInfo calls it too so the public write path cannot regress. TestWriteDiscoveryFile_RestrictsDirBeforeTrustingExistingFile drives the real function with a planted file whose endpoint answers /health with the matching nonce, so Discover reports StateRunning, startup aborts, and the test then asserts the chain is already protected. With only the pkg/api reordering reverted, that test fails with D:AI(A;OICIID;0x1301bf;;;WD) on both directories.

Intermediate directory. Both %LOCALAPPDATA%\toolhive and ...\toolhive\server are restricted now, outermost first. I stopped rewriting ACLs at the top of the chain ToolHive creates, since %LOCALAPPDATA% is not ours to re-permission, and documented the invariant at the top of permissions_windows.go: above the chain we rely on the OS default (user, SYSTEM, Administrators), and if another account does hold delete-child there, the ownership check turns the rename-aside attack into a startup failure instead of silent trust. If you would rather have the ancestor ACL audited in code, I will add it.

Ownership. Checked before the DACL write, so a directory somebody else owns is never half-secured, and again afterwards, so an owner that races the write cannot be reported as a completed lockdown. Fail closed rather than take ownership: taking ownership would repair the container while leaving the planted server.json inside it to be trusted. Trusted owners are the process user, SYSTEM, and Administrators. Administrators is in the set because a directory created by an elevated thv serve is owned by Administrators, and a standard user cannot make any of the three the owner of a directory they create. That is the one judgement call I would like you to sanity check.

Your findings together surfaced one more thing: with the directory fixed, an already-planted server.json was still trusted. Changing the parent DACL re-propagates inherited ACEs to existing children, but explicit ACEs and ownership survive, so readServerInfoFrom now refuses a discovery file owned by another account, alongside the existing symlink rejection. On startup that is self-healing rather than fatal.

Wording. Agreed and corrected in the PR body. CheckHealth sends a plain GET and compares the nonce from the response header, so nothing is transmitted to the probed endpoint. The description now states the risks as endpoint spoofing, persistent denial of startup, and interception or forgery of later client API traffic.

Tests. Added production-order (pkg/api), ancestor-replacement, hostile-owner (fail closed), and read-path rejection. Evidence in the PR body is from a Windows 11 box with real %LOCALAPPDATA% ACLs before and after.

Pushed 262b8ee and e9ef6a2 (DCO signed). Ready for another look when you have bandwidth.

Stan Shih (@stantheman0128)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Set explicit DACL on Windows discovery directory under %LOCALAPPDATA%

2 participants